Spring5源码解析-论Spring DispatcherServlet的生命周期
Spring Web框架架构的主要部分是DispatcherServlet
。也就是本文中重点介绍的对象。
在本文的第一部分中,我们将看到基于Spring的DispatcherServlet
的主要概念:前端控制器模式。第二部分将专门介绍Spring应用程序中的执行链。接下来是DispatcherServlet类
的解释。在最后一部分,我们将尝试开发一个自定义的dispatcher servlet
。
请注意,本文分析的DispatcherServlet来自Spring的5.0.0.RC3版本。如果使用不同的版本,则可能需要进行几个调整,其实因为分析的都是比较固定的东西,很少有改的。
什么是前端控制器模式?
在进入DispatcherServlet
之前,我们需要了解一些关于它的概念基础。DispatcherServlet
所隐含的关键概念其实就是前端控制器模式。
此模式为Web应用程序提供了一个中心入口点。该集中入口点将系统组件的共同特征进行重新组合。我们可以在那里找到安全资源,语言切换,会话管理,缓存或输入过滤的处理程序。这样做的一个很大的好处是:这个共同的入口点有助于避免代码重复。
因此,从技术上讲,前端控制器模式由一个捕获所有传入请求的类组成。之后,分析每个请求以知道哪个控制器以及哪个方法应该来处理该请求。
前端控制器模式有助于对以下询问做出最佳响应:
- 如何集中授权和认证?
- 如何处理正确的视图渲染?
- 如何使用URL重写映射将请求发送到适当的控制器?
这个前台控制器模式包含5名参与者:
- 客户端:发送请求。
- 控制器:应用程序的中心点,捕获所有请求。
- 调度员:管理视图的选择,以呈现给客户端。
- 视图:表示呈现给客户端的内容。
- 帮助:帮助查看和/或控制器完成请求处理。
什么是DispatcherServlet的执行链?
由标题可以看到,前端控制器模式有自己的执行链。这意味着它有自己的逻辑来处理请求并将视图返回给客户端:
请求由客户端发送。它到达作为Spring的默认前端控制器的
DispatcherServlet
类。DispatcherServlet
使用请求处理程序映射来发现将分析请求的控制器(controller)。接口org.springframework.web.servlet.HandlerMapping的实现返回一个包含org.springframework.web.servlet.HandlerExecutionChain类的实例。此实例包含可在控制器调用之前或之后调用的处理程序拦截器数组。你可以在Spring中有关于拦截器的文章中了解更多的信息。如果在所有定义的处理程序映射中找不到
HandlerExecutionChain
,这意味着Spring无法将URL与对应的控制器进行匹配。这样的话会抛出一个错误。现在系统进行拦截器预处理并调用由映射处理器找到的相应的controller(其实就是在找到的controller之前进行一波拦截处理)。在controller处理请求后,
DispatcherServlet
开始拦截器的后置处理。在此步骤结束时,它从controller接收ModelAndView实例(整个过程其实就是request请求
->进入interceptors
->controller
->从interceptors出来
->ModelAndView接收
)。DispatcherServlet现在将使用的该视图的名称发送到视图解析器。这个解析器将决定前台的展现内容。接着,它将此视图返回给DispatcherServlet,其实也就是一个“视图生成后可调用”的拦截器。
最后一个操作是视图的渲染并作为对客户端request请求的响应。
什么是DispatcherServlet?
通过上面讲到的前端控制器模式,我们可以很轻易的知道DispatcherServlet
是基于Spring
的Web
应用程序的中心点。它需要传入请求,并在处理程序映射,拦截器,控制器和视图解析器的帮助下,生成对客户端的响应。所以,我们可以分析这个类的细节,并总结出一些核心要点。
下面是处理一个请求时DispatcherServlet
执行的步骤:
1. 策略初始化
DispatcherServlet
是一个位于org.springframework.web.servlet包中的类,并扩展了同一个包中的抽象类FrameworkServlet
。它包含一些解析器的私有静态字段(用于本地化,视图,异常或上传文件),映射处理器:handlerMapping
和处理适配器:handlerAdapter
(进入这个类的第一眼就能看到的)。DispatcherServlet
非常重要的一个核心点就是是初始化策略的方法(protected void initStrategies(ApplicationContext context))。在调用onRefresh
方法时调用此方法。最后一次调用是在FrameworkServlet
中通过initServletBean
和initWebApplicationContext
方法进行的(initServletBean
方法中调用initWebApplicationContext
,后者调用onRefresh(wac)
)。initServletBean
通过所提供的这些策略生成我们所需要的应用程序上下文。其中每个策略都会产生一类在DispatcherServlet
中用来处理传入请求的对象。
基于篇幅,有些代码就不给贴示了,请在相应版本的源码中自行对照查找,此处只给一部分源码:
1 | /** |
需要注意的是,如果找的结果不存在,则捕获异常NoSuchBeanDefinitionException
(下面两段代码的第一段),并采用默认策略。如果在DispatcherServlet.properties文件中初始定义的默认策略不存在,则抛出BeanInitializationException异常(下面两段代码的第二段)。默认策略如下:
1 | /** |
抛出异常后调用getDefaultStrategy
(因为容器里都是单例的存在,所以只需要判断基于这个接口的默认实现实例size为1即可,两个以上还能叫默认么,都有选择了):
1 | /** |
2.请求预处理
FrameworkServlet
抽象类扩展了同一个包下的HttpServletBean
,HttpServletBean
扩展了javax.servlet.http.HttpServlet。点开这个类源码可以看到,HttpServlet
是一个抽象类,其方法定义主要用来处理每种类型的HTTP
请求:doGet(GET请求)
,doPost(POST)
,doPut(PUT)
,doDelete(DELETE)
,doTrace(TRACE)
,doHead(HEAD)
,doOptions(OPTIONS)
。FrameworkServlet
通过将每个传入的请求调度到processRequest(HttpServletRequest request,HttpServletResponse response)来覆盖它们。processRequest
是一个protected
和final
的方法,它构造出LocaleContext
和ServletRequestAttributes
对象,两者都可以在initContextHolders(request, localeContext, requestAttributes)
之后访问。所有这些操作的关键代码 请看:
1 |
|
3.请求处理
由上面所看到的,在processRequest
的代码中,调用initContextHolders方法后,调用protected void doService(HttpServletRequest request,HttpServletResponse response)。doService将一些附加参数放入request(如Flash映射:request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap())
,上下文信息:request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext())
等)中,并调用protected void doDispatch(HttpServletRequest request,HttpServletResponse response)。
doDispatch
方法最重要的部分是处理(handler
)的检索。doDispatch
调用getHandler()
方法来分析处理后的请求并返回HandlerExecutionChain
实例。此实例包含handler mapping
和`interceptors(拦截器)
。DispatcherServlet
做的另一件事是应用预处理程序拦截器(applyPreHandle())。如果至少有一个返回false
,则请求处理停止。否则,servlet
使用与 handler adapter
适配(其实理解成这也是个handler
就对了)相应的handler mapping
来生成视图对象。
doDispatch
方法:
1 | /** |
4.视图解析
获取ModelAndView
实例以查看呈现后,doDispatch
方法调用private void applyDefaultViewName(HttpServletRequest request,ModelAndView mv)。默认视图名称根据定义的bean名称,即viewNameTranslator
。默认情况下,它的实现是org.springframework.web.servlet.RequestToViewNameTranslator。这个默认实现只是简单的将URL转换为视图名称,例如(直接从RequestToViewNameTranslator
获取):http:// localhost:8080/admin/index.html将生成视图admin / index。
代码如下:
下一步是调用后置拦截器(其实就是出拦截器)做的一些处理。
1 | /** RequestToViewNameTranslator used by this servlet */ |
org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator
实现的org.springframework.web.servlet.RequestToViewNameTranslator
接口,其内对上段代码中getDefaultViewName
的实现为:
1 | public class DefaultRequestToViewNameTranslator implements RequestToViewNameTranslator { |
5.处理调度请求 - 视图渲染
现在,servlet
知道应该是哪个视图被渲染。它通过private void processDispatchResult(HttpServletRequest request,HttpServletResponse response,@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,@Nullable Exception exception)方法来进行最后一步操作 - 视图渲染。
首先,processDispatchResult
检查它们是否有参数传递异常。有一些异常的话,它定义了一个新的视图,专门用来定位错误页面。如果没有任何异常,该方法将检查ModelAndView实例
,如果它不为null
,则调用render
方法。
渲染方法protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception
。跳进此方法内部,根据定义的视图策略,它会查找得到一个View类
实例。它将负责显示响应。如果没有找到View
,则会抛出一个ServletException异常
。有的话,DispatcherServlet
会调用其render
方法来显示结果。
其实可以说成是后置拦截器(进入拦截器前置拦截处理->controller处理->出拦截器之前的此拦截器的后置处理),也就是在请求处理的最后一个步骤中被调用。
下面是processDispatchResult
和render(渲染)
的相关代码:
1 | /** |
1 | /** |
在这部分中,你需要记住的是我们定义了两个上下文:一个用于应用程序,另一个用于Web应用程序。他们有什么区别?应用程序上下文包含所有通用配置,比如service定义,数据库配置。Web应用程序上下文定义所有与Web相关的组件,比如controllers
或视图解析器。
Custom DispatcherServlet
我们已经了解了DispatcherServlet
的理论知识。通过文中的这些实用要点,我们可以编写自己的servlet来分派处理请求。同样的,我们也将按步进行,从捕获请求开始,以视图渲染结束。
通过上面的描述,为了捕获请求,我们需要覆盖doService
方法:
1 | public class CustomDispatcherServlet extends FrameworkServlet { |
这样,在我们的日志文件中,我们应该可以找到一条“[CustomDispatcherServlet]I got the request!”。接着,我们继续添加在DispatcherServlet
中doDispatch方法
所应该做的一些工作:
1 |
|
这个方法是做什么的?首先,它为构建一个Locale实例
用来接收请求。第二步是初始化org.springframework.web.context.request.ServletRequestAttributes实例。它是RequestAttributes
接口的实现,和本地化在同一级别。通过这个,我们可以访问servlet
请求的对象和会话对象,而不必区分会话和全局会话。最后,我们调用初始化context holders的initContextHolders()
方法,即从应用程序通过LocaleContextHolder
和RequestContextHolder
静态方法(分别为:getLocaleContext和getRequestAttributes
)访问请求属性和区域设置的对象。
当请求被拦截,一些基本的设置就绪的时候。我们发现我们还没有执行链和处理器适配器。我们可以通过以下代码进行:
1 | private HandlerExecutionChain getHandlerExecutionChain(HttpServletRequest request) throws Exception { |
通过执行链,我们可以通过 handler adapter将处理当前请求。看以下代码:
1 |
|
只有应用程序上下文中定义的适配器(this.handlerAdapter
)支持适配所生成的执行链(adapter.supports
)才可以返回我们想要的适配器。最后,我们可以返回到我们的doService
方法并操作它们来渲染视图:
1 | ModelAndView modelView = adapter.handle(request, response, executionChain.getHandler()); |
我们的servlet中简化了渲染。实际上,我们仅处理ModelAndView
的引用对象。这意味着ModelAndView
是一个String
的实例,用来表示要解析的视图模型,例如:我们定义好几个模板解析器(比如freemaker
,Thymeleaf
),然后查看其配置。在这个检查之后,我们迭代当前视图解析器。能够生成View实例的第一个解析器被视为处理过的请求中使用的解析器。最后,我们检查视图是否正确生成。拿到view实例后,我们调用其render()方法来在屏幕中显示请求处理结果。
在这部分中,我们将描述和代码部分限制在最低限度。只是为了把Spring的整个过程给集中呈现以下,达到更好的理解,其实就是在Servlet中的service方法内做些对request和response的文章而已了。
本文介绍了Spring Web应用程序的中心点,一个调度器servlet。请记住,它是一个处理所有传入请求并将视图呈现给用户的类。在重写之前,你应该熟悉执行链,handler mapping 或handler adapter等概念。请记住,第一步要做的是定义在调度过程中我们要调用的所有元素。handler mapping 是将传入请求(也就是它的URL)映射到适当的controller。最后提到的元素,一个handler适配器,就是一个对象,它将通过其内包装的handler mapping将请求发送到controller。此调度产生的结果是ModelAndView类的一个实例,后面被用于生成和渲染视图。
有问题可以加qq群523409180 讨论的